DragLimitMode = Enum{
    "None",
    "InsideParent",
    "ContainParent"
}
Drag = {
    name = "Drag",
    props = {
        dragLimitDirty = false,
        _dragLimitMode = DragLimitMode.None,
        _dragOffsetSensitive = true,
        xdraggable = true,
        ydraggable = true,
        _xMin = -math.huge,
        _xMax = math.huge,
        _yMin = -math.huge,
        _yMax = math.huge,
        _dragPercentX = 0,
        _dragPercentY = 0
    },
    gets = {
        dragLimitMode = function (self)
            return self._dragLimitMode
        end,
        dragOffsetSensitive = function (self)
            return self._dragOffsetSensitive
        end,
        dragPercentX = function (self)
            return self._dragPercentX
        end,
        dragPercentY = function (self)
            return self._dragPercentY
        end
    },
    sets = {
        dragLimitMode = function (self, key, val)
            self._dragLimitMode = val
        end,
        dragOffsetSensitive = function (self, key, val)
            self._dragOffsetSensitive = val
        end,
        dragPercentX = function (self, key, val)
            self._dragPercentX = val
            self:_setPositionByPercentX()
        end,
        dragPercentY = function (self, key, val)
            self._dragPercentY = val
            self:_setPositionByPercentY()
        end
    },
}

local keys = {"xMin", "yMin", "xMax", "yMax"}
for _, k in ipairs(keys) do
    Drag.gets[k] = load("return function(self, k) return self._"..k.." end")()
    Drag.sets[k] = load([[
        return function(self, k, v)
            self._]]..k..[[ = v
            self:setDragLimitChanged()
        end
    ]])()
end

function Drag:_setPositionByPercentX()
    self.x = math.lerp(self.xMin, self.xMax, self.dragPercentX)
end

function Drag:_setPositionByPercentY()
    self.y = math.lerp(self.yMin, self.yMax, self.dragPercentY)
end

function Drag:setDragLimitChanged()
    self.dragLimitChanged = true
end
function Drag:tryUpdateDragLimit()
    if self.dragLimitDirty then
        self:_updateDragLimit()
    end
end

function Drag:_updateDragLimit()
    if self.dragLimitMode == DragLimitMode.InsideParent then
        self:_updateLimitInsideParent()
    elseif self.dragLimitMode == DragLimitMode.ContainParent then
        self:_updateLimitContainParent()
    else
        self.xMin = -math.huge
        self.xMax = math.huge
        self.yMin = -math.huge
        self.yMax = math.huge
    end
    self.dragLimitDirty = false
end
local function setDragLimitDirty(self)
    self.dragLimitDirty = true
end
local onSetParent = function (self, parent, old)
    if old then
        old:off(NodeEvent.WidthDirty, setDragLimitDirty, self)
        old:off(NodeEvent.HeightDirty, setDragLimitDirty, self)
    end
    setDragLimitDirty(self)
    parent:on(NodeEvent.WidthDirty, setDragLimitDirty, self)
    parent:on(NodeEvent.HeightDirty, setDragLimitDirty, self)
end
function Drag:ctor()
    self.interactable = true
    self.dragStart = xy(0, 0)
    self.dragOffset = xy(0, 0)
    self:on(NodeEvent.WidthChanged, setDragLimitDirty, self)
    self:on(NodeEvent.HeightChanged, setDragLimitDirty, self)
    self:on(NodeEvent.Pressed, self.onDragStart, self)
    self:on(NodeEvent.Released, self.onDragEnd, self)
    self:on(NodeEvent.SetParent, onSetParent, self)
    if self.parent then
        onSetParent(self, self.parent)
    end
    self:on(NodeEvent.WidthDirty, setDragLimitDirty, self)
end

-- 高速滑动或者拖拽对象较小时，极易出现当前鼠标点不在拖拽对象上的情况，所以不能在对象上加滑动判定
EventDispatcher.global:on(NodeEvent.MouseMoved, function (x, y)
    if Drag.draggedItem then
        Drag.draggedItem:onDragMove(x, y)
    end
end)

EventDispatcher.global:on(NodeEvent.Released, function (x, y)
    if Drag.draggedItem then
        Drag.draggedItem:onDragEnd(x, y)
        Drag.draggedItem = nil
    end
end)

function Drag:_updateLimitInsideParent()
    self.xMin = self.parent:localLeft() - self:localLeft() * self.sx
    self.xMax = self.parent:localRight() - self:localRight() * self.sx
    self.yMin = self.parent:localTop() - self:localTop() * self.sy
    self.yMax = self.parent:localBottom() - self:localBottom() * self.sy
end

function Drag:_updateLimitContainParent()
    self.xMin = self.parent:localRight() - (1 - self.ax) * self.width * self.sx
    self.xMax = self.parent:localLeft() + self.width * self.ax * self.sx
    self.yMin = self.parent:localBottom() - (1 - self.ay) * self.height * self.sy
    self.yMax = self.parent:localTop() + self.height * self.ay * self.sy
end

function Drag:onDragStart(x, y)
    Drag.draggedItem = self
    self.dragStart:set(self.x, self.y)
    local lx, ly = self.parent:toLocal(x, y)
    self.dragOffset:set(self.x - lx, self.y - ly)
end

function Drag:onDragMove(x, y)
    local lx, ly = self.parent:toLocal(x, y)
    if self._dragOffsetSensitive then
        x, y = lx + self.dragOffset.x, ly + self.dragOffset.y
    else
        x, y = lx, ly
    end
    x, y = self:upatePositionByDragLimit(x, y)
    self._dragPercentX = (x - self.xMin) / (self.xMax - self.xMin)
    self._dragPercentY = (y - self.yMin) / (self.yMax - self.yMin)
    self:emit(NodeEvent.OnDragMoved, x, y)
end

function Drag:upatePositionByDragLimit(x, y)
    if self.xMin then
        x = math.max(self.xMin, x)
    end
    if self.xMax then
        x = math.min(self.xMax, x)
    end
    if self.yMin then
        y = math.max(self.yMin, y)
    end
    if self.yMax then
        y = math.min(self.yMax, y)
    end
    if self.xdraggable then
        self.x = x
    end
    if self.ydraggable then
        self.y = y
    end
    return x, y
end

function Drag:onDragEnd(x, y)
    self:onDragMove(x, y)
    self.dragged = false
    self:emit(NodeEvent.OnDragEnded, x, y)
end